﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using ICSharpCode.SharpZipLib;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;

namespace Quail.BinaryCompressedPackageFile
{
    [DataContract]
    public class BinaryCompressedPackageFile
    {
        private readonly string _fullfilename;
        private string _filename;
        private byte[] _data;
        private string _hash;

        public BinaryCompressedPackageFile(string filename, string hash, byte[] compresseddata)
        {
            _filename = Path.GetFileName(filename);
            _hash = hash;
            _data = compresseddata;
        }

        public BinaryCompressedPackageFile(string filename)
        {
            _fullfilename = filename;
            _filename = Path.GetFileName(filename);
        }

        [DataMember]
        public string Filename
        {
            get
            {
                return _filename;
            }

            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _filename = value;
            }

        }

        [DataMember]
        public byte[] BinaryData
        {
            get
            {
                if (_data == null)
                    LoadData();
                return _data;
            }

            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _data = value;
            }
        }

        [DataMember]
        public string Hash
        {
            get
            {
                if (_hash == null)
                    ComputeHash();
                return _hash;
            }

            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _hash = value;
            }
        }

        public void SetUncompressed(byte []data)
        {
            using (var memstream = new MemoryStream(data))
            {
                LoadStream(memstream);
            }
        }

        public byte[] GetUncompressed()
        {
            var data = BinaryData;

            try
            {
                try
                {
                    // compress using SharpZipLib - this works better for the QuailDashboardClient.exe
                    var memout = new MemoryStream();
                    var mem = new MemoryStream(data, false);

                    using (var czip = new InflaterInputStream(mem))
                    {
                        var buf = new byte[2048];
                        var read = czip.Read(buf, 0, 2048);

                        while (read > 0)
                        {
                            memout.Write(buf, 0, read);
                            read = czip.Read(buf, 0, 2048);
                        }

                        var result = memout.ToArray();
                        czip.Close();
                        mem.Close();
                        memout.Close();
                        return result;
                    }
                }
                catch (SharpZipBaseException)
                {
                    // try using C1 decompression for old files
                    var memout = new MemoryStream();
                    var mem = new MemoryStream(data, false);

                    using (var czip = new C1.C1Zip.C1ZStreamReader(mem, true))
                    {
                        var buf = new byte[2048];
                        var read = czip.Read(buf, 0, 2048);

                        while (read > 0)
                        {
                            memout.Write(buf, 0, read);
                            read = czip.Read(buf, 0, 2048);
                        }

                        var result = memout.ToArray();
                        czip.Close();
                        mem.Close();
                        memout.Close();
                        return result;
                    }
                }
            }
            catch (Exception e)
            {
                throw new Exception("Error decompressing " + _filename, e);
            }
        }

        private void LoadData()
        {
            using (var file = new FileStream(_fullfilename, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                LoadStream(file);
            }            
        }

        private void LoadStream(Stream file)
        {
            var mem = new MemoryStream();
            var czip = new DeflaterOutputStream(mem, new Deflater(Deflater.BEST_COMPRESSION));
            var hashalgo = SHA256.Create();

            try
            {
                var buf = new byte[2048];
                var read = file.Read(buf, 0, 2048);

                while (read > 0)
                {
                    czip.Write(buf, 0, read);
                    hashalgo.TransformBlock(buf, 0, read, buf, 0);
                    read = file.Read(buf, 0, 2048);
                }
                czip.Flush();
                czip.Close();
                czip = null;

                hashalgo.TransformFinalBlock(buf, 0, 0);

                _hash = Convert.ToBase64String(hashalgo.Hash);
                _data = mem.ToArray();
            }
            finally
            {
                if (czip != null)
                    czip.Close();
                mem.Close();
            }            
        }

        private void ComputeHash()
        {
            using (var file = new FileStream(_fullfilename, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                var hashalgo = SHA256.Create();
                _hash = Convert.ToBase64String(hashalgo.ComputeHash(file));
            }
        }

        public static List<BinaryCompressedPackageFile> GetAllFiles(string path, params string[] searchPatterns)
        {
            var list = new List<BinaryCompressedPackageFile>();

            foreach (var searchPattern in searchPatterns)
            {
                foreach (var file in Directory.GetFiles(path, searchPattern))
                {
                    list.Add(new BinaryCompressedPackageFile(file));
                }
            }

            return list;
        }

        public override bool Equals(object obj)
        {
            var other = obj as BinaryCompressedPackageFile;
            if (other == null)
                return false;

            if (_filename != other._filename || _hash != other._hash)
                return false;

            return _data.SequenceEqual(other._data);
        }

        public override int GetHashCode()
        {
            return _filename.GetHashCode() + _hash.GetHashCode();
        }

        public class Equality : IEqualityComparer<BinaryCompressedPackageFile>
        {
            /// <summary>
            /// When overridden in a derived class, determines whether two objects of type BinaryCompressedPackageFile are equal.
            /// </summary>
            /// <returns>
            /// true if the specified objects are equal; otherwise, false.
            /// </returns>
            /// <param name="x">The first object to compare.
            ///                 </param><param name="y">The second object to compare.
            ///                 </param>
            public bool Equals(BinaryCompressedPackageFile x, BinaryCompressedPackageFile y)
            {
                return x.Equals(y);
            }

            /// <summary>
            /// When overridden in a derived class, serves as a hash function for the specified object for hashing algorithms and data structures, such as a hash table.
            /// </summary>
            /// <returns>
            /// A hash code for the specified object.
            /// </returns>
            /// <param name="obj">The object for which to get a hash code.
            ///                 </param><exception cref="T:System.ArgumentNullException">The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null.
            ///                 </exception>
            public int GetHashCode(BinaryCompressedPackageFile obj)
            {
                return obj.GetHashCode();
            }
        }
    }
}
